package cn.xzqwjw.taskmanager.security.filter;

import cn.xzqwjw.taskmanager.security.TokenJwt;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/* 这个 Filter 对请求做拦截，主要是提取 header 中的 jwt
 然后跟登录一样提交给 AuthenticationManager 做检查
 AuthenticationManager 会调用相应的 AuthJwtProvider 做具体的验证
 这里继承了 OncePerRequestFilter ，如果通过了验证，后面就不需要验证了
*/

/**
 * @author rush
 */
public class AuthJwtFilter extends OncePerRequestFilter {

  /**
   * 定义一个 requiresAuthenticationRequestMatcher 需要验证请求的匹配器
   */
  private final RequestMatcher requestMatcher;

  /**
   * 定义一个请求允许的列表
   */
  private List<RequestMatcher> permissiveRequestMatcherList;

  /**
   * 定义验证请求核心管理器
   */
  private AuthenticationManager authManager;

  /**
   * 定义处理验证成功和失败后信息的Handler
   */
  private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
  private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

  public AuthJwtFilter() {
    // 拦截 header 中带 authorization 的请求
    this.requestMatcher = new RequestHeaderRequestMatcher("authorization");
  }

  /**
   * 先判断准备工作是否做好
   */
  @Override
  public void afterPropertiesSet() {
    Assert.notNull(authManager, "authenticationManager must be specified");
    Assert.notNull(successHandler, "AuthenticationSuccessHandler must be specified");
    Assert.notNull(failureHandler, "AuthenticationFailureHandler must be specified");
  }

  /**
   * 核心验证方法，验证请求里带的jwt令牌
   */
  @Override
  protected void doFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response,
                                  FilterChain filterChain)
      throws ServletException, IOException {
    // header 没带token的，直接放过，因为有些 url 匿名用户也可以访问
    // 如果需要不支持匿名用户的请求没带token，这里放过也没问题
    // 因为SecurityContext中没有认证信息，后面会被权限控制模块拦截
    if (!isRequiresAuth(request)) {
      filterChain.doFilter(request, response);
      return;
    }
    Authentication authResult = null;
    AuthenticationException authFailedException = null;
    try {
      TokenJwt jwtToken = new TokenJwt(getJwtToken(request));
      authResult = this.getAuthenticationManager().authenticate(jwtToken);
    } catch (AuthenticationException e) {
      authFailedException = e;
    }
    // 没有捕捉到异常，说明验证成功
    if (authResult != null) {
      // jwt 认证成功
      authSuccess(request, response, authResult);
    } else if (!isPermissiveRequest(request)) {
      // authResult 为 null，说明验证失败
      // 如果 jwt 认证失败，则先判断这个 request 不在例外列表里，才会返回错误
      authFailure(request, response, authFailedException);
      return;
    }
    // 执行下一个验证链
    filterChain.doFilter(request, response);
  }

  /**
   * 从请求中获取 jwt 令牌
   * 请求中的authorization携带的字符串是以 “Bearer + 一个空格 + jwt字符串”形式传递过来的
   */
  private String getJwtToken(HttpServletRequest request) {
    String jwtString = request.getHeader("authorization");
    String jwtStartString = "Bearer ";
    if (jwtString.startsWith(jwtStartString)) {
      return jwtString.substring(7);
    } else {
      return "";
    }
  }

  /**
   * 验证成功处理
   */
  private void authSuccess(HttpServletRequest request,
                           HttpServletResponse response,
                           Authentication authResult)
      throws IOException, ServletException {
    // 验证成功了就将验证结果放入上下文中
    // 后面的鉴权过程就会读取 SecurityContextHolder.getContext() 里的 authResult
    SecurityContextHolder.getContext().setAuthentication(authResult);
    successHandler.onAuthenticationSuccess(request, response, authResult);
  }

  /**
   * 验证失败处理
   */
  private void authFailure(HttpServletRequest request,
                           HttpServletResponse response,
                           AuthenticationException failed)
      throws IOException, ServletException {
    SecurityContextHolder.clearContext();
    failureHandler.onAuthenticationFailure(request, response, failed);
  }

  /**
   * 判断请求是否需要验证
   */
  private boolean isRequiresAuth(HttpServletRequest request) {
    return requestMatcher.matches(request);
  }

  /**
   * 将请求和预定义的不需要验证的url列表比对，判断该请求是否需要验证
   */
  private boolean isPermissiveRequest(HttpServletRequest request) {
    if (permissiveRequestMatcherList == null) {
      return false;
    }
    for (RequestMatcher permissiveMatcher : permissiveRequestMatcherList) {
      if (permissiveMatcher.matches(request)) {
        return true;
      }
    }
    return false;
  }

  /**
   * 设置匿名用户可访问 url，即那些不需要验证的 url 资源
   * 比如 "/api/login","/api/logout"，或者是那些静态资源的地址
   * 并将这些 url 存到 permissiveRequestMatcherList 里
   */
  public void setPermissiveUrl(String... urls) {
    if (permissiveRequestMatcherList == null) {
      permissiveRequestMatcherList = new ArrayList<>();
    }
    for (String url : urls) {
      permissiveRequestMatcherList.add(new AntPathRequestMatcher(url));
    }
  }

  private AuthenticationManager getAuthenticationManager() {
    return authManager;
  }

  public void setAuthenticationManager(AuthenticationManager authManager) {
    this.authManager = authManager;
  }

  public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
    Assert.notNull(successHandler, "successHandler cannot be null");
    this.successHandler = successHandler;
  }

  public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
    Assert.notNull(failureHandler, "failureHandler cannot be null");
    this.failureHandler = failureHandler;
  }

}
